package uc

import (
	"database/sql"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"
	"google.golang.org/protobuf/proto"
	"strconv"
	"strings"
	"sync"
	"wzgames/internal/constant"
	"wzgames/internal/gdao/walletdao"
	"wzgames/internal/gloader"
	"wzgames/internal/gmq"
	"wzgames/internal/model/mcomm"
	"wzgames/internal/model/statement"
	"wzgames/internal/model/user"
	"wzgames/internal/response"
	"wzgames/pkg/components/mysql"
	"wzgames/pkg/components/redis"
	"wzgames/pkg/logger"
	"wzgames/pkg/utils/dateutil"
)

var (
	ucMgr *UccManager
	onceC sync.Once
)

type UccManager struct {
	walletDs *mysql.DS
	redisC   *redis.RedisClient
	betsKey  string
}

func UccMgr() *UccManager {
	if ucMgr == nil {
		onceC.Do(func() {
			ucMgr = &UccManager{}
			ucMgr.walletDs = gloader.CommMgr().GetDBProxy(constant.MysqlOfWallet)
			ucMgr.redisC = gloader.CommMgr().RedisPool()

			var bd strings.Builder
			bd.WriteString(gloader.EnvString())
			bd.WriteString(constant.CacheBetIdKey)
			ucMgr.betsKey = bd.String()
		})
	}

	return ucMgr
}

// DouWalletImResultBet 双钱包及时投注结算
func (um *UccManager) DouWalletImResultBet(req *statement.PlayBet) (int16, *user.WalletEntity) {
	tx, err := um.walletDs.Ds().Beginx()
	if err != nil {
		logger.Error("[UC] create dou wallet im result bet fail on start tx", zap.Int64("AgtId", req.AgtId), zap.Uint64("UserID", req.Uid), zap.String("Currency", req.Curr), zap.Uint64("BetId", req.BetId), zap.Int64("InstID", req.InstId), zap.Error(err))
		return response.ErrInternal, nil
	}
	defer tx.Rollback()

	dao := walletdao.WalletDao{Driver: tx}
	wallet, e := dao.GetWalletByUid(req.Uid, req.CurrId)
	if e != nil || wallet == nil || wallet.Uid == 0 {
		logger.Error("[UC] create dou wallet im result bet fail on get wallet", zap.Int64("AgtId", req.AgtId), zap.Uint64("UserID", req.Uid), zap.String("Currency", req.Curr), zap.Uint64("BetId", req.BetId), zap.Int64("InstID", req.InstId), zap.Error(e))
		return response.ErrInternal, nil
	}
	dBalance, _ := decimal.NewFromString(wallet.Balance)
	dBetAmt, _ := decimal.NewFromString(req.BetAmt)
	if dBalance.LessThan(dBetAmt) {
		return response.ErrInsufficientBalance, nil
	}

	t := dateutil.UTC()
	dWinAmt, _ := decimal.NewFromString(req.ImRet.WinAmt)
	diff := dWinAmt.Sub(dBetAmt)

	wallet.Updated = t
	// 更新余额 乐观锁
	c, err1 := dao.IncWalletBalanceWithWv(wallet, diff.String())
	if err1 != nil || c == 0 {
		logger.Error("[UC] create dou wallet im result bet fail on UpdateWalletBalance", zap.Int64("AgtId", req.AgtId), zap.Uint64("UserID", req.Uid), zap.String("Currency", req.Curr), zap.Uint64("BetId", req.BetId), zap.Int64("InstID", req.InstId), zap.Error(err1))
		return response.ErrInternal, nil
	}
	newBalance := dBalance.Add(diff) // 新余额
	if newBalance.IsNegative() {
		return response.ErrInsufficientBalance, nil
	}
	balanceAfterBet := dBalance.Sub(dBetAmt)
	wallet.Balance = newBalance.String()
	wallet.Wv += 1
	// 投注
	charge := &user.WalletBetCharge{
		BetId:   req.BetId,
		InstId:  req.InstId,
		Uid:     req.Uid,
		CurrId:  req.CurrId,
		GameId:  req.GameId,
		BetAmt:  req.BetAmt,
		EffAmt:  req.EffAmt,
		Fee:     req.Fee,
		BetTime: req.Dateline,
		Balance: balanceAfterBet.String(),
		Wv:      wallet.Wv,
		Created: t,
		Updated: t,
	}
	err = dao.CreateBetCharge(charge)
	if err != nil {
		logger.Error("[UC] create dou wallet im result bet fail on CreateBetCharge", zap.Int64("AgtId", req.AgtId), zap.Uint64("UserID", req.Uid), zap.String("Currency", req.Curr), zap.Uint64("BetId", req.BetId), zap.Int64("InstID", req.InstId), zap.Error(err))
		return response.ErrInternal, nil
	}
	// 派彩
	payoutEnt := &user.WalletBetPayout{
		BetId:   req.BetId,
		Win:     req.ImRet.Win,
		Amt:     req.ImRet.WinAmt,
		GAct:    req.ImRet.GameAct,
		PayTime: req.ImRet.Dateline,
		Created: t,
		Updated: t,
	}
	err = dao.CreateBetPayout(payoutEnt)
	if err != nil {
		logger.Error("[UC] create dou wallet im result bet payout fail on CreateBetPayout", zap.Int64("AgtId", req.AgtId), zap.Uint64("UserID", req.Uid), zap.String("Currency", req.Curr), zap.Uint64("BetId", req.BetId), zap.Int64("InstID", req.InstId), zap.Error(err))
		return response.ErrInternal, nil
	}

	betPending := &user.WalletBetPending{
		BetId:   req.BetId,
		NodeId:  req.NodeId,
		GAct:    req.GameAct,
		Stm:     0, // 流水待生成
		BetTime: req.Dateline,
		Uid:     req.Uid,
		CurrId:  req.CurrId,
		GameId:  req.GameId,
		RoundId: req.RoundId,
		BetAmt:  req.BetAmt,
		EffAmt:  req.EffAmt,
		Fee:     req.Fee,
		InstId:  req.InstId,
	}
	err = dao.CreateBetPending(betPending)
	if err != nil {
		logger.Error("[UC] create dou wallet im result bet pending fail on CreateBetPending", zap.Int64("AgtId", req.AgtId), zap.Uint64("UserID", req.Uid), zap.String("Currency", req.Curr), zap.Uint64("BetId", req.BetId), zap.Int64("InstID", req.InstId), zap.Error(err))
		return response.ErrInternal, nil
	}

	err = tx.Commit()
	if err == nil {
		return 0, wallet
	}

	logger.Error("[UC] create dou wallet im result bet fail on tx commit", zap.Int64("AgtId", req.AgtId), zap.Uint64("UserID", req.Uid), zap.String("Currency", req.Curr), zap.Uint64("BetId", req.BetId), zap.Int64("InstID", req.InstId), zap.Error(err))

	return response.ErrInternal, nil
}

// DouWalletBet 投注待派彩的
func (um *UccManager) DouWalletBet(req *statement.PlayBet) (int16, *user.WalletEntity) {
	tx, err := um.walletDs.Ds().Beginx()
	if err != nil {
		logger.Error("[UC] create dou wallet bet fail on start tx", zap.Int64("AgtId", req.AgtId), zap.Uint64("UserID", req.Uid), zap.String("Currency", req.Curr), zap.Uint64("BetId", req.BetId), zap.Int64("InstID", req.InstId), zap.Error(err))
		return response.ErrInternal, nil
	}
	defer tx.Rollback()

	dao := walletdao.WalletDao{Driver: tx}
	wallet, e := dao.GetWalletByUid(req.Uid, req.CurrId)
	if e != nil || wallet == nil || wallet.Uid == 0 {
		logger.Error("[UC] create dou wallet bet fail on get wallet", zap.Int64("AgtId", req.AgtId), zap.Uint64("UserID", req.Uid), zap.String("Currency", req.Curr), zap.Uint64("BetId", req.BetId), zap.Int64("InstID", req.InstId), zap.Error(e))
		return response.ErrInternal, nil
	}
	dBalance, _ := decimal.NewFromString(wallet.Balance)
	dBetAmt, _ := decimal.NewFromString(req.BetAmt)
	if dBalance.LessThan(dBetAmt) {
		return response.ErrInsufficientBalance, nil
	}

	t := dateutil.UTC()
	wallet.Updated = t
	// 扣除投注额
	c, err1 := dao.DecWalletBalance(wallet, req.BetAmt)
	if err1 != nil {
		logger.Error("[UC] create dou wallet bet fail on UpdateWalletBalance", zap.Int64("AgtId", req.AgtId), zap.Uint64("UserID", req.Uid), zap.String("Currency", req.Curr), zap.Uint64("BetId", req.BetId), zap.Int64("InstID", req.InstId), zap.Error(err1))
		return response.ErrInternal, nil
	}
	if c == 0 {
		return response.ErrInsufficientBalance, nil
	}
	dBalance = dBalance.Sub(dBetAmt)
	wallet.Balance = dBalance.String()
	wallet.Wv += 1
	charge := &user.WalletBetCharge{
		BetId:   req.BetId,
		InstId:  req.InstId,
		Uid:     req.Uid,
		CurrId:  req.CurrId,
		GameId:  req.GameId,
		BetAmt:  req.BetAmt,
		EffAmt:  req.EffAmt,
		Fee:     req.Fee,
		BetTime: req.Dateline,
		Balance: dBalance.String(),
		Wv:      wallet.Wv,
		Created: t,
		Updated: t,
	}
	err = dao.CreateBetCharge(charge)
	if err != nil {
		logger.Error("[UC] create dou wallet bet fail on CreateBetCharge", zap.Int64("AgtId", req.AgtId), zap.Uint64("UserID", req.Uid), zap.String("Currency", req.Curr), zap.Uint64("BetId", req.BetId), zap.Int64("InstID", req.InstId), zap.Error(err))
		return response.ErrInternal, nil
	}

	betPending := &user.WalletBetPending{
		BetId:   req.BetId,
		NodeId:  req.NodeId,
		GAct:    req.GameAct,
		Stm:     0,
		BetTime: req.Dateline,
		Uid:     req.Uid,
		CurrId:  req.CurrId,
		GameId:  req.GameId,
		RoundId: req.RoundId,
		BetAmt:  req.BetAmt,
		EffAmt:  req.EffAmt,
		Fee:     req.Fee,
		InstId:  req.InstId,
	}
	err = dao.CreateBetPending(betPending)
	if err != nil {
		logger.Error("[UC] create dou wallet bet pending fail on CreateBetPending", zap.Int64("AgtId", req.AgtId), zap.Uint64("UserID", req.Uid), zap.String("Currency", req.Curr), zap.Uint64("BetId", req.BetId), zap.Int64("InstID", req.InstId), zap.Error(err))
		return response.ErrInternal, nil
	}

	err = tx.Commit()
	if err == nil {
		return 0, wallet
	}

	logger.Error("[UC] create dou wallet bet fail on tx commit", zap.Int64("AgtId", req.AgtId), zap.Uint64("UserID", req.Uid), zap.String("Currency", req.Curr), zap.Uint64("BetId", req.BetId), zap.Int64("InstID", req.InstId), zap.Error(err))

	return response.ErrInternal, nil
}

// DouWalletPayout 派彩
func (um *UccManager) DouWalletPayout(req *statement.PlayPayout) (int16, string) {
	tx, err := um.walletDs.Ds().Beginx()
	if err != nil {
		logger.Error("[UC] create dou wallet payout fail on start tx", zap.Uint64("BetId", req.BetId), zap.Error(err))
		return response.ErrInternal, ""
	}
	defer tx.Rollback()

	dao := walletdao.WalletDao{Driver: tx}
	// 查询投注日志
	bp, e := dao.GetBetPending(req.BetId)
	if e != nil {
		if e != sql.ErrNoRows {
			logger.Error("[UC] create dou wallet payout fail on get bet charge", zap.Uint64("BetId", req.BetId), zap.Error(e))
			return response.ErrInternal, ""
		}
		return response.ErrNotFound, ""
	}

	t := dateutil.UTC()
	if req.WinAmt != "0" && req.WinAmt != "" {
		// 派彩 更新余额
		c, err1 := dao.IncWalletBalance(bp.Uid, bp.CurrId, t, req.WinAmt)
		if err1 != nil || c == 0 {
			logger.Error("[UC] create dou wallet payout fail on IncWalletBalance", zap.Uint64("BetId", req.BetId), zap.Error(err1))
			return response.ErrInternal, ""
		}
	}
	// 派彩
	payout := &user.WalletBetPayout{
		BetId:   req.BetId,
		Amt:     req.WinAmt,
		Win:     req.Win,
		GAct:    req.GameAct,
		PayTime: req.Dateline,
		Created: t,
		Updated: t,
	}
	err = dao.CreateBetPayout(payout)
	if err != nil {
		logger.Error("[UC] create dou wallet payout fail on CreateBetPayout", zap.Uint64("BetId", req.BetId), zap.Error(err))
		return response.ErrInternal, ""
	}
	// 设置投注状态为结算，流水待生成
	_, _ = dao.UpdateBetPendingGAct(bp.BetId, req.GameAct)

	err = tx.Commit()
	if err == nil {
		return 0, req.WinAmt
	}

	logger.Error("[UC] create dou wallet payout fail on tx commit", zap.Uint64("BetId", req.BetId), zap.Error(err))

	return response.ErrInternal, ""
}

// DouWalletBetCancel 取消投注
func (um *UccManager) DouWalletBetCancel(req *statement.PlayCancel) (int16, string) {
	tx, err := um.walletDs.Ds().Beginx()
	if err != nil {
		logger.Error("[UC] create dou wallet cancel fail on start tx", zap.Uint64("BetId", req.BetId), zap.Error(err))
		return response.ErrInternal, ""
	}
	defer tx.Rollback()

	dao := walletdao.WalletDao{Driver: tx}
	bp, e := dao.GetBetPending(req.BetId) // 查询投注日志
	if e != nil {
		if e != sql.ErrNoRows {
			logger.Error("[UC] create dou wallet cancel fail on get bet charge", zap.Uint64("BetId", req.BetId), zap.Error(e))
			return response.ErrInternal, ""
		}
		return response.ErrNotFound, ""
	}

	t := dateutil.UTC()
	// 还反投注额
	c, err1 := dao.IncWalletBalance(bp.Uid, bp.CurrId, t, bp.BetAmt)
	if err1 != nil || c == 0 {
		logger.Error("[UC] create dou wallet cancel fail on IncWalletBalance", zap.Uint64("BetId", req.BetId), zap.Error(err1))
		return response.ErrInternal, ""
	}
	// 投注取消日志
	betCancel := &user.WalletBetCancel{
		BetId:    req.BetId,
		Amt:      bp.BetAmt,
		Dateline: req.Dateline,
		Created:  t,
		Updated:  t,
	}
	err = dao.CreateBetCancel(betCancel)
	if err != nil {
		logger.Error("[UC] create dou wallet bet cancel fail on CreateBetCancel", zap.Uint64("BetId", req.BetId), zap.Error(err))
		return response.ErrInternal, ""
	}
	// 投注状态 设置为取消，并取消流水生成
	_, _ = dao.UpdateBetPendingGActWithStm(bp.BetId, mcomm.GameAction_GameCancel)

	err = tx.Commit()
	if err == nil {
		return 0, bp.BetAmt
	}

	logger.Error("[UC] create dou wallet cancel fail on tx commit", zap.Uint64("BetId", req.BetId), zap.Error(err))

	return response.ErrInternal, ""
}

// DouWalletBetStep 投注 分步骤操作，如Mines、Goal、HiLo等
func (um *UccManager) DouWalletBetStep(req *statement.PlayStep) int16 {
	dao := walletdao.WalletDao{Driver: um.walletDs.Ds()}
	_, err := dao.UpdateBetPendingGAct(req.BetId, mcomm.GameAction_GameUpStep)
	if err != nil {
		logger.Error("[UC] save play bet step fail on UpdateBetPendingGAct", zap.Uint64("BetId", req.BetId), zap.Error(err))
		return response.ErrInternal
	}

	return 0
}

// BetStmSettled 批量设置投注的流水状态为已生成
func (um *UccManager) BetStmSettled(ids string, nodeId int64) int16 {
	dao := walletdao.WalletDao{Driver: um.walletDs.Ds()}
	_, err := dao.UpdateBetPendingStm(ids)
	if err != nil {
		logger.Error("[UC] update bet pending stm fail on UpdateBetPendingStm", zap.Int64("NodeId", nodeId), zap.String("BetIds", ids), zap.Error(err))
		return response.ErrInternal
	}

	return 0
}

// AvtLoseSettle 批量设置小飞机输的投注的流水状态为已生成
func (um *UccManager) AvtLoseSettle(ids string, nodeId int64) int16 {
	dao := walletdao.WalletDao{Driver: um.walletDs.Ds()}
	_, err := dao.UpdateBetPendingGActWithStmIn(ids, mcomm.GameAction_GameLoseSettle)
	if err != nil {
		logger.Error("[UC] update avt bet lose settle fail on UpdateBetPendingGActWithStmIn", zap.Int64("NodeId", nodeId), zap.String("BetIds", ids), zap.Error(err))
		return response.ErrInternal
	}

	return 0
}

// DoNodeStartSettledStm 游戏节点启动后批量处理已结算未生成流水的
func (um *UccManager) DoNodeStartSettledStm(req *statement.NodeStartStmSettle) (int16, int64, uint64) {
	dao := walletdao.WalletDao{Driver: um.walletDs.Ds()}
	ret, err := dao.GetBetPendingStmUnsettle(req.NodeId, req.Start, req.LimitNum, req.LastBetId)
	if err != nil {
		logger.Error("[UC] get bet pending unsettle stm fail on GetBetPendingStmUnsettle", zap.Int64("NodeId", req.NodeId), zap.Int64("Start", req.Start), zap.Uint64("LastBetId", req.LastBetId), zap.Error(err))
		return response.ErrInternal, 0, 0
	}
	l := len(ret)
	if l == 0 {
		return 0, 0, req.LastBetId
	}
	var idsArr []string
	pubPkg := &statement.SettleStmList{
		NodeId:   req.NodeId,
		Dateline: req.Dateline,
		Op:       1,
	}
	for _, bp := range ret {
		idsArr = append(idsArr, strconv.FormatUint(bp.BetId, 10))
		pubPkg.States = append(pubPkg.States, &statement.StmState{
			BetId:   bp.BetId,
			GAct:    bp.GAct,
			GameId:  bp.GameId,
			RoundId: bp.RoundId,
		})
	}
	lastBetId := ret[l-1].BetId
	b, ex := proto.Marshal(pubPkg)
	if ex != nil {
		logger.Error("[UC] do node start settle stm fail on proto.Marshal", zap.Int64("NodeId", req.NodeId), zap.Int64("Start", req.Start), zap.Uint64("LastBetId", req.LastBetId), zap.Error(err))
		return response.ErrInternal, 0, 0
	}
	if !gmq.Publish(constant.SubjectSettleStm, b) {
		return response.ErrInternal, 0, 0
	}
	_, err = dao.UpdateBetPendingStm(strings.Join(idsArr, ","))
	if err != nil {
		return response.ErrInternal, 0, 0
	}

	return 0, int64(l), lastBetId
}
